/**
 * 
 */
package pxb.openid.springsecurity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.openid.OpenIDAttribute;
import org.springframework.security.openid.OpenIDAuthenticationStatus;
import org.springframework.security.openid.OpenIDAuthenticationToken;
import org.springframework.security.openid.OpenIDConsumer;
import org.springframework.security.openid.OpenIDConsumerException;

import pxb.openid.Association;
import pxb.openid.Constants;
import pxb.openid.ServletUtil;
import pxb.openid.ns.Ax;
import pxb.openid.rp.Associater;
import pxb.openid.rp.Auth;
import pxb.openid.rp.Discoverer;
import pxb.openid.rp.Verifier;
import pxb.openid.rp.servlet.BaseOpenidServlet;
import pxb.util.http.HttpConnector;

/**
 * @author panxiaobo
 * 
 */
public class POpenidConsumer implements OpenIDConsumer {

	public POpenidConsumer(List<OpenIDAttribute> attributes) {
		this();
		this.attributesToFetch = Collections.unmodifiableList(attributes);
	}

	@Autowired(required = false)
	protected Discoverer discover = new Discoverer();
	@Autowired(required = false)
	protected Associater associater = new Associater();
	@Autowired(required = false)
	protected Auth auth = new Auth();
	@Autowired(required = false)
	protected Verifier verifier = new Verifier();
	@Autowired(required = false)
	@Qualifier("pxb.openid.openid2server.cache")
	protected Map<String, String> openid2ServerCache = new HashMap<String, String>();
	@Autowired(required = false)
	@Qualifier("pxb.openid.server2handle.cache")
	protected Map<String, String> server2HandleCache = new HashMap<String, String>();
	@Autowired(required = false)
	@Qualifier("pxb.openid.association.cache")
	protected Map<String, Association> associationCache = new HashMap<String, Association>();

	private static final Logger log = LoggerFactory.getLogger(BaseOpenidServlet.class);

	public POpenidConsumer() {
		super();
		try {
			HttpConnector conn = (HttpConnector) Class.forName("pxb.util.http.impl.CommonsHttpClientImpl").newInstance();
			discover.setHttpConnector(conn);
			log.info("using {} as HttpConnector", conn);
		} catch (Throwable e) {
			log.debug("Some thing error while loading pxb.util.http.impl.CommonsHttpClientImpl as HttpConnector", e);
			log.info("using default HttpConnector");
		}
		log.info("use default redirect");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.openid.OpenIDConsumer#beginConsumption(javax.servlet.http.HttpServletRequest,
	 * java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public String beginConsumption(HttpServletRequest req, String id, String returnToUrl, String realm) throws OpenIDConsumerException {
		String server = openid2ServerCache.get(id);
		if (server == null) {
			try {
				server = discover.discover(id);
			} catch (Exception e) {
				log.warn("Error Discovering Provider for openid :" + id, e);
				throw new OpenIDConsumerException("Error Discovering Provider for openid :" + id, e);
			}
			openid2ServerCache.put(id, server);
		}
		String handle = server2HandleCache.get(server);
		if (handle == null || "".equals(handle)) {
			Association associate = associater.associate(server);
			handle = associate.getId();
			server2HandleCache.put(server, associate.getId());
			associationCache.put(associate.getId(), associate);
		}
		Association associate = associationCache.get(handle);
		if (associate == null || associater.isExpired(associate)) {
			associate = associater.associate(server);
			server2HandleCache.put(server, associate.getId());
			associationCache.put(associate.getId(), associate);
		}

		return auth.generate(associate, extraParams(), server, id, returnToUrl).toURL();
	}

	/**
	 * @return
	 */
	private Map<String, String> extraParams() {
		if (!attributesToFetch.isEmpty()) {
			Ax ax = new Ax().request();
			for (OpenIDAttribute attr : attributesToFetch) {
				ax.askFor(attr.getName(), attr.getType(), attr.isRequired());
			}
			return ax.getMap();
		}
		return null;
	}

	private List<OpenIDAttribute> attributesToFetch = Collections.emptyList();

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.openid.OpenIDConsumer#endConsumption(javax.servlet.http.HttpServletRequest)
	 */
	@Override
	public OpenIDAuthenticationToken endConsumption(HttpServletRequest req) throws OpenIDConsumerException {
		String mode = req.getParameter(Constants.OPENID_MODE);
		if ("id_res".equals(mode)) {
			Map<String, String> values = ServletUtil.getRequestArgs(req);
			String handle = values.get(Constants.OPENID_ASSOCIATION_HANDLE);
			Association associate = associationCache.get(handle);
			try {
				Map<String, String> trusted = verifier.verify(associate, values);
				String openid = (String) trusted.get(Constants.OPENID_IDENTITY);
				if (Constants.ID_SELECT.equals(openid)) {
					openid = (String) trusted.get(Constants.OPENID_CLAIMED_ID);
				}
				return success(req, openid, trusted, values);
			} catch (Exception ex) {
				log.warn("openid verify failed", ex);
				// throw new RuntimeException("openid verify failed", ex);
				return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.FAILURE, (String) null, "openid verify failed", null);
			}
		} else if ("error".equals(mode)) {
			Map<String, String> values = ServletUtil.getRequestArgs(req);
			// throw new RuntimeException("error: " + values.get("openid.error"));
			return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.ERROR, (String) null, values.get("openid.error"), null);
		} else if ("cancel".equals(mode)) {
			// throw new RuntimeException("cancel by user");
			return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.CANCELLED, (String) null, "cancel by user", null);
		} else {
			// throw new RuntimeException("Unknow openid.mode :" + mode);
			return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.FAILURE, (String) null, "unknow error", null);

		}
	}

	/**
	 * @param req
	 * @param openid
	 * @param trusted
	 * @param values
	 * @return
	 */
	private OpenIDAuthenticationToken success(HttpServletRequest req, String openid, Map<String, String> trusted, Map<String, String> values) {
		List<OpenIDAttribute> attributes = new ArrayList<OpenIDAttribute>(this.attributesToFetch.size());
		if (Ax.hasAx(trusted)) {
			Ax ax = new Ax(trusted);
			for (OpenIDAttribute attr : attributesToFetch) {
				List<String> axValues = new ArrayList<String>();
				axValues.add(ax.getAxValue(attr.getType()));
				if (!axValues.isEmpty()) {
					OpenIDAttribute fetched = new OpenIDAttribute(attr.getName(), attr.getType(), axValues);
					fetched.setRequired(attr.isRequired());
					attributes.add(fetched);
				}
			}
		}

		return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.SUCCESS, openid, "some message", attributes);

	}

}
